<script setup lang="ts">
import { computed, ref, ComputedRef, provide, watch, Ref, PropType, onMounted } from "vue"
import Field from "./fields/type"
import string from './fields/type/string'
import { FormModel, FieldsModle, FieldsType, FiedStatusModel, Status, GroupStateModel, StatusModel, LabelPosition } from "./types"

const props = defineProps({
  model: Object as PropType<FormModel>,
  modelValue: Object as PropType<any>,
  labelWidth: {
    type: [Number, String]
  },
  labelPosition: {
    type: String as PropType<LabelPosition>,
    validator(value: string) {
      return ['left', 'right', 'top'].includes(value)
    },
    default: 'right'
  },
  inline: {
    type: [Number, String],
    validator(value: string) {
      return ['', '1', '2', '3', '4', 1, 2, 3, 4].includes(value)
    }
  }
})


const emit = defineEmits<{
  (e: 'fieldInput', code: string, value: any, oldvalue: any): void
  (e: 'fieldChange', code: string, value: any, oldvalue: any): void
  (e: 'input', value: any): void
  (e: 'fieldStatusChange', obj: any): void
  (e: 'confDataSource', value: boolean): void
}>()

const status: Status = {
  view: false,
  valid: false,
}
const fieldStatus: Ref<FiedStatusModel> = ref({} as FiedStatusModel)
const valid: Ref<boolean> = ref(true)
const errorMessage: Ref<string> = ref('')
const groupState: Ref<GroupStateModel> = ref({})
const labelMaxWidth: Ref<number> = ref(0)

const fieldMap = computed(() => {
  const map: { [key: string]: FieldsModle } = {}
  for (const field of fields.value || []) {
    map[field.code as string] = field
  }
  return map
})
const classes = computed(() => {
  return [
    'dynamic-form',
    `form-label-${props.labelPosition}`,
    {
      ...(props.inline && {
        'dynamic-form-inline': ['', '1', '2', '3', '4', 1, 2, 3, 4].includes(
          props.inline
        )
      })
    }
  ]
})
const labelStyles = computed(() => {
  const style: { [key: string]: string } = {}
  if (props.labelWidth || labelMaxWidth.value) {
    style.width = `${props.labelWidth || labelMaxWidth.value}px`
  }
  return style
})
const fields: ComputedRef<FieldsModle[]> = computed(() => {
  return props.model ? props.model.fields.filter((v: FieldsModle) => !v.hide) : [];
});

watch(() => props.modelValue, (v) => {
  if (v === undefined) {
    emit('input', {})
  }
})

watch(() => props.model, (v) => {
  if (v?.fields) {
    v.fields.forEach((val: FieldsModle) => {
      ['objStruct', 'arrStruct', 'form'].includes(val.type) && (groupState.value[val.code as string] = true)
    })
  }
})

const formItemStyles = (span: number) => {
  const style: any = {}
  if (props.inline !== undefined) {
    const fpx = span
      ? (1 / Number(props.inline)) * span * 100
      : (1 / Number(props.inline)) * 100
    style.flexBasis = `${fpx}%`
    style.margin = '11px 0'
    if (props.labelPosition === 'top') style.padding = '0 10px'
  }
  return style
}


const fieldControl = (field: FieldsModle) => {
  const type = field.type || "string";
  if (Field[type as FieldsType]) return Field[type as FieldsType].component;
  else {
    console.warn(
      `没有${type}类型的表单控件,请换一个类型或检查type是否拼写错误`
    );
  }
};

const fieldClass = (field: FieldsModle) => {
  const res = {
    "form-item": true,
    ["field-type-" + field.type]: true,
  };
  return res;
}

const fieldStyle = (field: FieldsModle) => {
  const res: { flex?: number } = {}
  if (field.span) {
    res.flex = field.span
  }
  return res
}

const field = ref()
const validate = (list: string[]) => {
  return Promise.all(
    field.value
      .filter((field: { model: { code: string; }; validate: any; }) => {
        return list ? (field.model && list.includes(field.model.code) && field.validate) : field.validate
      })
      .map((field: { validate: () => any; }) => field.validate())
  ).then(res => {
    valid.value = true
    errorMessage.value = ''
    res.forEach(({ valid, message }) => {
      if (!valid) {
        valid.value = false
        if (message && !errorMessage.value.length) {
          errorMessage.value = message
        }
      }
    })
    return {
      valid: valid.value,
      message: errorMessage.value,
      data: props.modelValue.value
    }
  })
}

const setValue = (code: string, value: any) => {
  if (code) {
    const oldValue = props.modelValue[code]
    if (value !== oldValue) {
      props.modelValue[code] = value
      emit('fieldChange', code, props.modelValue[code], oldValue)
    }
  } else if (code === undefined) {
    const oldValue = props.modelValue[code]
    if (value !== oldValue) {
      emit('input', value)
    }
  }
}

const inputValue = (code: string, value: any) => {
  const oldValue = props.modelValue[code]
  emit('fieldInput', code, value, oldValue)
  setValue(code, value)
}

const setFieldStatus = (code: string, statusCode: keyof StatusModel, value: any) => {
  if (!fieldStatus.value[code]) {
    initFiedStatus(code)
  }
  const oldValue = fieldStatus.value[code][statusCode]
  // @ts-ignore
  fieldStatus.value[code][statusCode] = value
  emit('fieldStatusChange', {
    code: code,
    status: statusCode,
    value: value,
    oldValue: oldValue,
    control: thisObj
  })
}
const initFiedStatus = (code: string) => {
  const field = fieldMap.value[code]
  let status = {}
  if (field) {
    status = {
      disbaled: field.disbaled === true,
      enable: field.disbaled !== true,
      show: field.show !== false,
      pure: true,
      dirty: false,
      valid: true,
      error: [],
      errorMessage: null,
      tip: field.tip,
      tooltip: field.tooltip,
      placeholder: field.placeholder,
      validators: []
    }
  }
  fieldStatus.value[code] = status as StatusModel
}

const setArr = (code: string) => {
  if (code && !props.modelValue[code]) {
    props.modelValue[code] = [{}]
    return true
  } else {
    return true
  }
}
const arrStructModel = (model: FormModel | FormModel[], index: number) => {
  return Array.isArray(model) ? model[index] : model
}
const addChild = (index: number, code: string) => {
  const item = JSON.parse(JSON.stringify(props.modelValue[code][index]))
  props.modelValue[code].push(item)
}
const removeChild = (index: number, code: string) => {
  props.modelValue[code].splice(index, 1)
}
const toggleState = (code: string) => {
  groupState.value[code] = !groupState.value[code]
}
const reset = () => {
  field.value.forEach((field: { reset: () => any; }) => {
    field.reset && field.reset()
  })
}
// dragValue组件触发数据配置调用
const confDataSource = () => {
  emit('confDataSource', true)
}
const formLabel = ref()
const updateLabelWidth = () => {
  if (formLabel.value) {
    labelMaxWidth.value = Math.max(...formLabel.value.map((v: { offsetWidth: any; }) => v.offsetWidth)) + 6
  }
}


const thisObj = {
  model: props.model,
  modelValue: props.modelValue,
  labelWidth: props.labelWidth,
  labelPosition: props.labelPosition,
  inline: props.inline,
  status: status,
  fieldStatus: fieldStatus.value,
  valid: valid.value,
  errorMessage: errorMessage.value,
  groupState: groupState.value,
  labelMaxWidth: labelMaxWidth.value,
  fieldMap: fieldMap.value,
  classes: classes.value,
  labelStyles: labelStyles.value,
  fields: fields.value,
  formItemStyles,
  fieldControl,
  fieldClass,
  fieldStyle,
  validate,
  inputValue,
  setValue,
  setFieldStatus,
  initFiedStatus,
  setArr,
  arrStructModel,
  addChild,
  removeChild,
  toggleState,
  reset,
  confDataSource,
  updateLabelWidth
}

provide('form', thisObj)
provide('root', thisObj)

onMounted(() => {
  updateLabelWidth()
})
</script>

<template>
  <div class="dynamic-form">
    <div v-for="field of fields" :key="field.code" :class="fieldClass(field)"
      :style="formItemStyles(field.span as number)">
      <div v-show="field.name" ref="formLabel" :class="['form-label', { 'form-label-group': field.type === 'group' }]">
        {{ field.name }}
      </div>
      <div v-if="field.type !== 'group'" class="form-control">
        <component v-model="props.modelValue[field.code as string]" ref="field" :model="field"
          :is="fieldControl(field)">
        </component>
        <div v-if="field.tip" class="form-item-tip">{{ field.tip }}</div>
      </div>
    </div>
  </div>
</template>

<style lang="less">
@import "./style/index.less";
</style>
